/**
 * SPDX-FileCopyrightText: © 2020 Liferay, Inc. <https://liferay.com>
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */

/* eslint-disable no-console */

const {createHash} = require('crypto');
const fs = require('fs');
const JSZip = require('jszip');
const path = require('path');

const {qaDir, testDir} = require('./resources');
const {logStep} = require('./util');

const IGNORED_ITEMS = ['.liferay.json', 'node_modules', 'yarn.lock'];

async function check(projectDirName) {
	const expectedDir = path.join(qaDir, 'expected', projectDirName);

	if (!fs.existsSync(expectedDir)) {
		return;
	}

	logStep(`CHECK: ${projectDirName}`);

	const actualDir = path.join(testDir, projectDirName);

	const failed = await diff(expectedDir, actualDir, {});

	if (failed) {
		console.log('');
		console.log('Legend:');
		console.log('  + Unexpected file/dir generated by the build');
		console.log('  - Expected file/dir missing from build');
		console.log('  * File contents in build differ from the expected');
		console.log('');
		console.log('Project check failed.');

		process.exit(1);
	}
}

async function diff(expectedDir, actualDir, tokens) {
	let somethingChanged = false;

	if (!fs.existsSync(actualDir)) {
		console.log('-', actualDir);

		return true;
	}

	const expectedItems = fs.readdirSync(expectedDir);
	const actualItems = fs
		.readdirSync(actualDir)
		.filter((item) => !IGNORED_ITEMS.includes(item));

	for (const actualItem of actualItems) {
		if (!expectedItems.includes(actualItem)) {
			somethingChanged = true;
			console.log('+', path.join(actualDir, actualItem));
		}
	}

	for (const expectedItem of expectedItems) {
		const actualFile = path.resolve(actualDir, expectedItem);
		const expectedFile = path.resolve(expectedDir, expectedItem);

		if (fs.statSync(expectedFile).isDirectory()) {
			const somethingInsideChanged = await diff(
				expectedFile,
				actualFile,
				tokens
			);

			somethingChanged = somethingChanged || somethingInsideChanged;
		}
		else if (!actualItems.includes(expectedItem)) {
			somethingChanged = true;
			console.log('-', path.join(actualDir, expectedItem));
		}
		else {
			if (
				expectedFile.endsWith('.zip') ||
				expectedFile.endsWith('.jar')
			) {
				const somethingInsideChanged = await diffZip(
					expectedFile,
					actualFile,
					tokens
				);

				somethingChanged = somethingChanged || somethingInsideChanged;
			}
			else {
				const somethingInsideChanged = diffText(
					expectedFile,
					actualFile,
					tokens
				);

				somethingChanged = somethingChanged || somethingInsideChanged;
			}
		}
	}

	return somethingChanged;
}

function diffText(expectedFile, actualFile, tokens) {
	const expectedContent = readExpected(expectedFile, tokens);
	const actualContent = fs.readFileSync(actualFile);

	const expectedHash = createHash('sha256')
		.update(expectedContent)
		.digest('hex');
	const actualHash = createHash('sha256').update(actualContent).digest('hex');

	if (expectedHash !== actualHash) {
		console.log('*', actualFile);

		return true;
	}

	/*
	console.log('<<<<<<<<<<<<<<<<<<');
	console.log(expectedContent);
	console.log('==================');
	console.log(actualContent.toString());
	console.log('>>>>>>>>>>>>>>>>>>');
	*/

	return false;
}

async function diffZip(expectedFile, actualFile) {
	const expectedZip = await JSZip.loadAsync(fs.readFileSync(expectedFile));
	const actualZip = await JSZip.loadAsync(fs.readFileSync(actualFile));

	const expectedItems = readZipItems(expectedZip);
	const actualItems = readZipItems(actualZip);

	const result = {};

	for (const path of Object.keys(expectedItems)) {
		if (!actualItems[path]) {
			result[path] = '-';
		}
	}

	for (const path of Object.keys(actualItems)) {
		if (!expectedItems[path]) {
			result[path] = '+';
		}
	}

	for (const path of Object.keys(expectedItems)) {
		const expectedContent = await expectedItems[path].async('string');
		const actualContent = await actualItems[path].async('string');

		if (expectedContent !== actualContent) {
			result[path] = '*';
		}
	}

	if (Object.keys(result).length) {
		console.log('*', `${actualFile}:`);

		for (const path of Object.keys(result)) {
			console.log(`    ${result[path]} ${path}`);
		}

		return true;
	}

	return false;
}

function readZipItems(zip) {
	const items = {};

	zip.forEach((relativePath, item) => {
		items[relativePath] = item;
	});

	return items;
}

function readExpected(file, tokens) {
	const tokenized = fs.existsSync(`${file}.TOKENIZE`);

	if (tokenized) {
		file += '.TOKENIZE';
	}

	let content = fs.readFileSync(file);

	if (!tokenized) {
		return content;
	}

	content = content.toString();

	Object.entries(tokens).forEach(([key, value]) => {
		content = content.replace(new RegExp(`{{${key}}}`, 'g'), value);
	});

	return content;
}

module.exports = check;
